package br.edu.ufcg.splab.simulator;



import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;

import br.edu.ufcg.splab.core.InterfaceGraph;
import br.edu.ufcg.splab.core.InterfaceVertex;
import br.edu.ufcg.splab.exceptions.LTSBTException;

/**
 * @author Mayza Nunes
 * 
 * <P>This class is responsible for generating one set of graphs dynamically and record a log of the generation. 
 * For this they receives the  parameters of the set of graphs to be generated,as the 
 * <br>amount of graphs, 
 * The number of the graphs to be generate.
 * <br>Depth, 
 * 	The maximum depth of the generated LTS,in other words,the biggest level between the root and the final state.
 * <br>number of {@link br.edu.ufcg.splab.simulator.StructuralPattern#loop loops},
 * <br>number of {@link br.edu.ufcg.splab.simulator.StructuralPattern#join joins},
 * <br>number of {@link br.edu.ufcg.splab.simulator.StructuralPattern#fork forks}.
 * 
 * <br>And for generating each graph,LTSSimulator is strongly dependent on the class {@link  br.edu.ufcg.splab.generator.LTSGenerator Generator}, which dynamically generates graphs.
 */

public class LTSSimulator {

	private LTSGenerator generator;
	private StringBuffer log;
	private List<StringBuffer> buffers;
	private static final String EOL = System.getProperty("line.separator");
	private int  depth;

	public LTSSimulator() {
	this.generator = new LTSGenerator();
	}
	/**
	 * The main method of the simulator. Gets the list of the dynamically generated LTSs .
	 * 
	 * @param depth
	 *            The maximum depth of the generated LTS,in other words,the biggest level between the root and the final state.
	 * @param quantity
	 *            The number of generated LTSs.
	 * @param nLoops
	 *            Quantity of  {@link br.edu.ufcg.splab.simulator.StructuralPattern#loop loops} to be inserted in the LTSs.           
	 * @param nJoins
	 *            Quantity of  {@link br.edu.ufcg.splab.simulator.StructuralPattern#join joins} to be inserted in the LTSs.
	 * @param nForks
	 *            Quantity of  {@link br.edu.ufcg.splab.simulator.StructuralPattern#fork forks} to be inserted in the LTSs.
	 * @throws LTSBTException 
	 */
	public List<InterfaceGraph> getSimulatedLTS(int depth, int quantity, int nLoops,
			int nJoins, int nForks) throws LTSBTException{
		this.buffers = new ArrayList<StringBuffer>();
		this.depth = depth;
		List<InterfaceGraph> resultLTS = new ArrayList<InterfaceGraph>();
		List<StructuralPattern> structures = this.getStructuralPatterns(nLoops,
				nJoins, nForks);

		for (int i = 0; i < quantity; i++) {
			this.log = new StringBuffer();
			this.generator = new LTSGenerator();
			// Generate the sequence in the specified depth
			this.generateInitialSequence(depth);
			Collections.shuffle(structures);
			this.executeOperations(structures);
			resultLTS.add(this.generator.getGraphGenerator());
			this.buffers.add(new StringBuffer(this.log.toString()));
		}
		
		return resultLTS;
	}

	/**
	 * Get the list of {@link br.edu.ufcg.splab.simulator.StructuralPattern structural patterns} to be inserted in the LTS.
	 * 
	 * @param numberOfLoops
	 *            Quantity of {@link br.edu.ufcg.splab.simulator.StructuralPattern#loop loops} to be inserted in the LTSs.
	 * @param numberOfjoins
	 *            Quantity of {@link br.edu.ufcg.splab.simulator.StructuralPattern#join joins} to be inserted in the LTSs.
	 * @param numberOfForks
	 *            Quantity of {@link br.edu.ufcg.splab.simulator.StructuralPattern#loop forks} to be inserted in the LTSs.
	 * @return
	 * 		The list of structures
	 */
	private List<StructuralPattern> getStructuralPatterns(int numberOfLoops,
			int numberOfjoins, int numberOfForks) {
		List<StructuralPattern> patterns = new ArrayList<StructuralPattern>();

		for (int i = 0; i < numberOfLoops; i++) {
			patterns.add(StructuralPattern.loop);
		}
		for (int i = 0; i < numberOfjoins; i++) {
			patterns.add(StructuralPattern.join);
		}
		for (int i = 0; i < numberOfForks; i++) {
			patterns.add(StructuralPattern.fork);
		}
		return patterns;
	}

	/**
	 * The method receives a list of structures(loops,joins and forks) to put in the graph and execute
	 *  the operations in the LTS being generated.
	 * 
	 * @param structures
	 *            The structures to be inserted in order.
	 * @param depth
	 *            The maximum depth to the LTS.
	 * @throws LTSBTException 
	 *
	 */
	private void executeOperations(List<StructuralPattern> structures) throws LTSBTException{
		for (StructuralPattern structuralPattern : structures) {
			switch (structuralPattern) {
			case join:
				this.putAJoin();
				break;
			case fork:
				this.putAFork();
				break;
			case loop:
				this.putALoop();
				break;
			}
		}
	}
	/**
	 * Randomly gets the source vertex and the sink vertex to add a loop and save the log of the transition.
	 * @throws LTSBTException 
	 * 
	 * @throws Exception 
	 * 		Exceptions are thrown when:
	 * 		-The source vertex is a leaf<br>
	 * 		-The source vertex is the root<br>
	 * 		-The source vertex and the sink vertex are neighbors<br>
	 * 		-The level of the source vertex is smaller than the level of a sink vertex
	 */
	private void putALoop() throws LTSBTException{
		List<InterfaceVertex> vertices =  this.getVertices();
		InterfaceVertex first =  vertices.get(new Random().nextInt(vertices.size()));
		ArrayList<InterfaceVertex> visitados = new ArrayList<InterfaceVertex>();
		generator.getPossibleLoop(first, visitados);
		InterfaceVertex second ;
		 // checks if source vertex has predecessors:
		 // if has,choose one of them randomly
		 // if not, the second is set as root and algorithm generate another vertex first
		 
		if(visitados.size() > 0){
			 second = visitados.get(new Random().nextInt(visitados.size()));	
		}else{
			second = this.generator.getGraphGenerator().getRoot();	
		}
		this.log.append("loop: " + first.getLabel() + "->" + second.getLabel());
		// Verifies that the first is not leaf neither root,
		// if the points are not neighbors either if the first level is greater than the second
		
		if (!first.isLeaf() && !first.getInTransitions().isEmpty() && !areAdjacents(first, second)
				&& this.generator.getLevels().get(first)> this.generator.getLevels().get(second)){
			this.generator.addLoop(first, second);
			this.log.append(EOL);	
		}else{
			this.log.append(" fail" +EOL);
			this.putALoop();	
			
		}	
	}

	/**
	 * Randomly gets a source vertex to add a fork and save the log of the transition. 
	 * Two vertices successors are created to be connected to the source vertex.
	 * @throws LTSBTException 
	 * 
	 */
	private void putAFork() throws LTSBTException{
		List<InterfaceVertex> vertices =  this.getVertices();
		InterfaceVertex first =  vertices.get(new Random().nextInt(vertices.size()));
		this.log.append("fork: " + first.getLabel());
		if (this.generator.getLevels().get(first) < depth) {
			this.generator.addBranch(first);	
			this.log.append(EOL);
		} else {
			this.log.append(" fail" +EOL);
			putAFork();
		}		

	}

	/**
	 * Randomly GetS  two vertices in the graph to add a join creating other vertex to be connected. 
	 * And save the log of the transition. 
	 * @throws LTSBTException 
	 * 
	 *@throws Exception 
	 *			Exceptions are thrown when:
	 *			-The two source vertices of the join are equals<br>
	 *			-The two source vertices are adjacent<br>
	 *			-The level of sources vertices is smaller than depth of a graph<br>
	 *
	 */
	private void putAJoin() throws LTSBTException{
		List<InterfaceVertex> vertices =  this.getVertices();
		InterfaceVertex first =  vertices.get(new Random().nextInt(vertices.size()));
		InterfaceVertex second =  vertices.get(new Random().nextInt(vertices.size()));
		this.log.append("join: " + first.getLabel());
		this.log.append(" and ");
		this.log.append( second.getLabel());
		if (!first.equals(second) && !this.areAdjacents(first, second)
				&& this.generator.getLevels().get(first) < depth && 
				this.generator.getLevels().get(second) < depth) {
			this.generator.addJoin(first, second);
			this.log.append(EOL);

		}  else {
			this.log.append(" fail");
			this.log.append(EOL);
			putAJoin();
		}



	}

	/**
	 * Tests if the given vertexes are adjacent.
	 * 
	 * @param first
	 *            The source vertex to test.
	 * @param second
	 *            The sink vertex to test.
	 * @return
	 * 		True if they are adjacent and false if not	
	 */
	private boolean areAdjacents(InterfaceVertex first, InterfaceVertex second) {
		for (int i = 0 ;i < first.getOutTransitions().size() ; i++) {
			if (first.getOutTransitions().get(i).getTo().equals(second)) {
				return true;
			}
		}
		for (int j = 0 ; j < second.getOutTransitions().size() ; j++) {
			if (second.getOutTransitions().get(j).getTo().equals(first)) {
				return true;
			}
		}
		return false;
	}


	/**
	 * Generate the minimal first LTS satisfying the depth requirement. It is the same to the all graphs.
	 * For example,for one graph with depth 4,it generates :
	 * 	 
	 * <P> <img alt="Loop"  src="../../../../../pictures/initialsequence.jpg">	 
	 * 
	 * @param depth
	 *            The maximum depth of the LTS.
	 * @throws LTSBTException 
	 * 			throws exception if an error occurs when you use the method addPath
	 */
	private void generateInitialSequence(int depth) throws LTSBTException{
		InterfaceVertex vertice = this.generator.getGraphGenerator().getRoot();
		this.generator.getGraphGenerator().getStates().put(vertice.getUUID(), vertice);
		for (int i = 0; i < depth; i++) {
			this.generator.addPath(vertice);
			vertice =  vertice.getOutTransitions().get(0).getTo();
			this.generator.getGraphGenerator().getStates().put(vertice.getUUID(), vertice);
		}		
	}

	/**
	 * Get the list of InterfaceVextex that there are in graph.
	 * 
	 * @return list of vertex
	 * 
	 */
	private List<InterfaceVertex> getVertices(){
		List<InterfaceVertex> result = new ArrayList<InterfaceVertex>(this.generator.getLevels().keySet());
		return result;
	}
	
	public Map<InterfaceVertex,Integer> getLevels(){
		return this.generator.getLevels();
	}
	
	/**
	 * Method responsible for return the list of string with the graph logs
	 * @return
	 * 		The list string with the graph logs,each string in the list keeps logs of a distinct graph.
	 */
	public List<StringBuffer> getBuffers() {
		return buffers;
	}
	

}